﻿using DXBase;
using DXBase.D3D;
using DXBase.Samples;
using DXBase.Scenes;
using DXBase.Structs;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI;

namespace Sample2_HitTesting
{
	public class HitTestScene : D3DScene
	{
		public HitTestScene()
		{
			Background = Colors.DarkSlateBlue;
			StartCamera = new Camera()
			{
				EyeLocation = new Vector3F(0, 0.0f, 0.0f),
				LookVector = new Vector3F(0, 0, 100),
				UpVector = new Vector3F(0, 1, 0),
				FarPlane = 800,
			};
			origin = new Vector3F(0, 0, 120);
			CameraController.ModelTransform.Origin = origin;
			sphere = MeshLoader.CreateGeosphere32(1, 3);
			texEarth = new Texture2D();
			texEarth.LoadFile("DXBase.Utils\\Samples\\earth600.jpg");
			texRed = new Texture2D();
			texRed.LoadFile("Assets\\red.png");
			pixelShader = new PixelShader();
			pixelShader.LoadShaderFile("DXBase.Utils.Shaders\\BasicPixelShader.cso");
			vertexShader = new VertexShader();
			vertexShader.LoadShaderFile(BasicVertex.GetLayout(), "DXBase.Utils.Shaders\\BasicVertexShader.cso");

			var box = new Box3D(-1, 2, -1, 2, -1, 2);
			items = new HitItem[3];
			items[0] = new HitItem
			{
				BaseBox = box,
				Name = "Medium",
				ModelTransform = CameraController.ModelTransform,
				ObjectTransform = new ModelTransform3D { Scale = new Vector3F(8, 8, 8), LeftHanded = sphere.LeftHanded },
			};
			items[1] = new HitItem
			{
				BaseBox = box,
				Name = "Large",
				ModelTransform = CameraController.ModelTransform,
				ObjectTransform = new ModelTransform3D { Scale = new Vector3F(12, 12, 10), LeftHanded = sphere.LeftHanded },
			};
			items[2] = new HitItem
			{
				BaseBox = box,
				Name = "Small",
				ModelTransform = CameraController.ModelTransform,
				ObjectTransform = new ModelTransform3D { Scale = new Vector3F(6, 8, 6), LeftHanded = sphere.LeftHanded },
			};
			inside = new bool[items.Length];

			sIndex = new OctTree<HitItem>(new Box3D(origin + new Vector3F(-90, -90, -90), origin + new Vector3F(90, 90, 90)), 4.0f);
			foreach (var item in items)
			{
				item.UpdateAndTransformAndBounds();
				sIndex.Add(item);
			}
		}

		OctTree<HitItem> sIndex;
		Vector3F origin;
		PixelShader pixelShader;
		VertexShader vertexShader;
		Shape sphere;
		Texture2D texEarth, texRed;
		HitItem[] items;
		bool[] inside;
		SamplerState sampler = new SamplerState(); // needed for texture
		RasterizerState rstate = new RasterizerState(); // wireframe

		RasterizerState hitRaster = new RasterizerState() { CULL_MODE = CULL_MODE.NONE }; // wireframe
		DepthStencilState lessEqualDSS = new DepthStencilState() { DepthFunc = COMPARISON_FUNC.LESS_EQUAL };

		public RasterizerState RasterizerState { get { return rstate; } }
		public IEnumerable CullModes { get { return Enum.GetValues(typeof(CULL_MODE)); } }

		public override void Update(DXContext ctxt, PerformanceTimer ptimer)
		{
			base.Update(ctxt, ptimer);

			UpdateTransforms(ctxt, ptimer);
			FrustumTest();

			var mtrail = Inputcontroller != null ? Inputcontroller.GetMouseTrailOrOther() : null;
			if (mtrail != null && !mtrail.Handled)
			{
				mtrail.Handled = true;
				HitTest(ctxt, mtrail);
			}
		}

		void UpdateTransforms(DXContext ctxt, PerformanceTimer ptimer)
		{
			float t = ptimer.Total;
			Action<int, ModelTransform3D> evolve = (i, model) =>
			{
				switch (i)
				{
					case 0:
						model.Rotation = QuaternionF.Rotation(0, 1, 0, 24 * t);
						model.Location = origin + new Vector3F(20, 0, 0).Rotate(0.2f, 1, 0, 8 * t);
						break;
					case 1:
						model.Rotation = QuaternionF.Rotation(0, 1, 0, 24 * t);
						model.Location = origin + new Vector3F(45, 0, 0).Rotate(-0.2f, 1, 0.3f, 12 * t);
						break;
					case 2:
						model.Rotation = QuaternionF.Rotation(0, 1, 0, 36 * t);
						model.Location =
							origin + new Vector3F(45, 0, 0).Rotate(-0.2f, 1, 0.3f, 12 * t)
							+ new Vector3F(18, 0, 0).Rotate(0.2f, 1.2f, 0.3f, 18 * t);
						break;
				}
			};
			for (int i = 0; i < items.Length; i++)
			{
				var it = items[i];
				var b0 = it.Bounds;
				evolve(i, it.ObjectTransform);
				it.UpdateAndTransformAndBounds();
				sIndex.Updated(it, b0);
			}
		}

		void FrustumTest()
		{
			var frustum = CameraController.Camera.GetViewFrustum();
			for (int i = 0; i < items.Length; i++)
			{
				var it = items[i];
				bool isInside = frustum.Intersects(it.Bounds);
				if (inside[i] != isInside)
				{
					inside[i] = isInside;
					Debug.WriteLine(it.Name + " Inside " + isInside);
				}
			}
			var sFound = sIndex.Query(b => frustum.Intersects(b)).ToList();
			var dFound = Enumerable.Range(0, inside.Length).Where(i => inside[i]).Select(i => items[i]).ToList();
			if (sFound.Count != dFound.Count)
				Debugger.Break();
		}

		void HitTest(DXContext ctxt, InputController.Trail mtrail)
		{
			var mmt = CameraController.ModelTransform.Transform;
			var p = mtrail.DownPoint.CurrentPoint.Position;
			var ray = CameraController.Camera.RayAt(CoordinateType.DIP, ctxt, (float)p.X, (float)p.Y);

			object hitlock = new object();
			float min = 0;
			hitMesh = null;
			Parallel.ForEach<HitItem, Tuple<HitItem, Ray.HitMeshResult>>(sIndex.Query(b => ray.IntersectBox(b)), 
				() => null,
				(it, state, local) =>
				{
					// convert ray to mesh coordinate,
					// instead of converting ALL vertices to world transform!!!
					var mray = it.Transform.Invert() * ray;
					var result = mray.IntersectMesh(EnumSphereTriangle());
					if (!result.Hit)
						return local;
					if (local == null)
						return Tuple.Create(it, result);
					if (result.Distance < local.Item2.Distance)
						return Tuple.Create(it, result);
					return local;
				},
				r =>
				{
					if (r == null)
						return;
					lock (hitlock)
					{
						if (hitMesh == null || r.Item2.Distance < min)
						{
							hitMesh = r.Item1;
							iHitTriangle = r.Item2.Triangle;
							min = r.Item2.Distance;
						}
					}
				}
			);
		}
		HitItem hitMesh;
		int iHitTriangle;

		IEnumerable<Triangle3D> EnumSphereTriangle()
		{
			if (sphere.Topology != Topology.Trianglelist) throw new ArgumentOutOfRangeException();
			Pointer<uint> indices = new PointerUInt32(sphere.Indices);
			Pointer<BasicVertex> vertices = new PointerBasicVertex(sphere.Vertices[0]);
			for (int i = 0, N = (int)sphere.GetIndexCount(); i < N - 2; i += 3)
				yield return new Triangle3D(
					vertices[(int)indices[i]].Pos,
					vertices[(int)indices[i + 1]].Pos,
					vertices[(int)indices[i + 2]].Pos
				);
		}

		public override void Render(D3DGraphic g)
		{
			g.Clear(Background);
			g.Shape = sphere;
			g.SetShaderResource(ShaderType.Pixel, texEarth);
			g.SetStateSampler(sampler);
			g.SetStateRasterizer(rstate);
			g.PixelShader = pixelShader;
			g.VertexShader = vertexShader;

			g.SetConstantBuffers(4, ShaderType.Pixel | ShaderType.Vertex, PerObjectData.Buffer);
			g.SetConstantBuffers(0, ShaderType.Pixel, PerFrameData.Buffer, DirLights.Buffer, PointLights.Buffer, SpotLights.Buffer);

			CameraController.Camera.SetProjection(g.Context);
			var pod = new PerObjectData
			{
				projection = CameraController.Camera.Projection,
				DrawFlags = DrawFlag.UseTexel,
			};

			// we don't do frustum culling, we test it, but we could!
			//var frustum = CameraController.Camera.GetViewFrustum();
			//foreach (var tr in sIndex.Query(b => frustum.Intersects(b)))
			foreach (var tr in items)
			{
				pod.WorldModel = CameraController.Camera.View * tr.Transform;
				pod.WorldModelNorm = CameraController.Camera.View * tr.NormalTransform;
				PerObject = pod;
				g.DrawIndexed();
			}

			// draw hit triangle
			if (hitMesh != null)
			{
				g.SetStateRasterizer(hitRaster);
				g.SetStateDepthStencil(lessEqualDSS);

				pod.WorldModel = CameraController.Camera.View * hitMesh.Transform;
				pod.WorldModelNorm = CameraController.Camera.View * hitMesh.NormalTransform;
				pod.DrawFlags = DrawFlag.UseFixColor;
				pod.FixColor = Colors.Red;
				PerObject = pod;

				g.DrawIndexed(3, (uint)(3 * iHitTriangle), 0);
				g.SetStateDepthStencil(null);
			}
		}
	}
}
